Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

23.Gün - SwiftUI Proje-3 Bölüm-1

Bu proje ile SwiftUI’nin neden view’lar için struct kullanıldığını, some View ’in neden bu kadar çok kullanıldığını ve modifier’ların tam olarak nasıl çalıştığını inceleyeceğiz.

SwiftUI’de Neden Struct Kullanılmaktadır? #

Eğer UIKit’i kullandıysanız, view’lar için struct yerine class kullanıldığını farketmişsinizdir. SwiftUI’de ise böyle bir durum söz konusu değil, genel olarak view’lar için struct kullanmayı tercih edeceğiz. Peki ama neden?

İlk olarak, bir performans unsuru vardır: struct’lar sınıflardan daha basit ve daha hızlıdır. Fakat bu struct kullanılmasının ana nedeni değildir.

UIKit’de her view, arka plan rengi, nasıl konumlandırılacağını belirleyen constraints’ler gibi birçok property ve method’u olan UIView adlı bir sınıftan geliyordu. Bunlar çok sayıda ve her UIView ihtiyacı olmasa bile bunlara sahip olmak zorundaydı, çünkü kalıtım bu şekilde çalışır.

SwiftUI’da tüm view’lar önemsiz struct’lardır ve neredeyse maliyetsizce oluşturulabilirler. Bir düşünelim, tek bir tamsayı tutan bir struct yaparsak struct’ımızın tüm boyutu o tek tamsayıdır. Üst sınıflardan miras alınan sürpriz ekstra değerler yok.

Modern iPhone’ların gücü sayesinde, 1000 SwiftUI view’ı, hatta 100.000 SwiftUI görünümünü rahatlıkla oluşturabiliriz, o kadar hızlıdır ki düşünmeye değmez.

Performans önemli olsa da struct olan view’lar için çok daha önemli bir şey var: bizi, state’i temiz bir şekilde izole etme konusunda düşünmeye zorluyor. Sınıflar değerlerini serbestçe değiştirebilirler, bu da daha karmaşık bir koda yol açabilir.

SwiftUI, zaman içinde değişmeyen view’lar üreterek bizi daha işlevsel bir tasarım yaklaşımına geçmeye teşvik ediyor.

Buna karşılık Apple’ın UIView dokümanında , UIView’ın sahip olduğu yaklaşık 200 property ve method listelenmektedir ve bunların tümüne ihtiyaç duyulsa da duyulmasa da alt sınıflara aktarılmaktadır.

SwiftUI’de view için bir sınıf kullanırsak kodumuzun derlenmediğini veya çalışma zamanında çöktüğünü görebiliriz.

Ana SwiftUI view Arkasında Ne Var? #

SwiftUI projesi oluşturduğumuzda şu kod karşımıza gelir;

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

Daha sonra bu VStack’e bir arka plan rengi eklemek ve ekranı doldurmasını beklemek yaygındır;

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
        .background(.red)
    }
}

First SwiftUI View

Ancak beklediğimiz gerçekleşmiyor. Bunun yerine, ekranın ortasında küçük kırmızı bir görüntü ve onun ötesinde beyaz bir boşluk görüyoruz.

Bu durum insanların kafasını karıştırır ve genellikle “view’ın arkasındaki alanın kırmızıya dönmesini nasıl sağlarım?” sorusuna yol açar.

Aslında view’ın arkasında hiçbir şey yok. Bu beyaz alanı geçici çözümler ile kırmızıya çevirmeye çalışmamalısınız.

Content View’ın arakasında UIHostingController adı verilen bir şey var: UIKit ile SwiftUI arasındaki köprüdür. Ancak bunu değiştirmeye çalışırsak, kodumuzun çalışmadığını ve aslında gelecekte bir noktada iOS’ta tamamen çalışmayı durdurabileceğini göreceksiniz.

Bunun yerine, view’ın arkasında hiçbir şey olmadığını düşünelim.

Bu düşünceye sahip olduğumuzda, doğru çözüm VStack’in daha fazla yer kaplamasını sağlamaktır. İçeriğinin etrafını doldurmak yerine ekranı doldurmasını sağlayabiliriz. Bunu frame() modifier’ını kullanarak, hem maksimum genişliği hem de maksimum yüksekliği için .infinity değerini girerek yapabiliriz.

Dolayısıyla padding() modifier’ını şu şekilde değiştirelim;

.frame(maxWidth: .infinity, maxHeight: .infinity)

swiftui full background

maxWidth ve maxHeight kullanmak, width ve height kullanmaktan farklıdır. VStack’in tüm alanı kaplaması gerektiğini söylemiyoruz, sadece kaplayabileceğini söylüyoruz. Eğer etrafta başka görünümler varsa, SwiftUI hepsinin yeterli alana sahip olmasını sağlayacaktır.

SwiftUI Modifier Sırası Önemli Mi? #

Bir SwiftUI view’ına neredeyse her modifier uygulandığında aslında bu değişikliğin uygulandığı yeni bir view oluşturururuz, sadece mevcut view’ı yerinde değiştirmeyiz. View’lar yalnızca onlara verdiğimiz tam özellikleri tutar, bu nedenle arka plan rengini veya yazı tipini ayarlarsak bu verileri depolayacak bir yer yoktur.

Şu kodu inceleyelim;

Button("Hello, world!") {
    // do nothing
}    
.background(.red)
.frame(width: 200, height: 200)

swiftui button 1

Ortasında “Hello, world!” yazan 200x200 boyutlarında kırmızı bir buton görmeyeceğiz. Bunun yerine yukarıdaki gibi, ortasında “Hello, world!” yazan ve yazının hemen etrafında kırmızı dikdörtgen bulunan 200x200 boyutlarında boş bir kare göreceğiz.

Modifier’ların her biri, view üzerinde bir özellik ayarlamak yerine, o modifier’ın uygulandığı yeni bir yapı oluşturur.

View’ın body’sinin türünü sorarak, SwiftUI’nin iç yüzüne göz atabiliriz;

Button("Hello, world!") {
    print(type(of: self.body))
}    
.background(.red)
.frame(width: 200, height: 200)

type(of:) fonksiyonu belirli bir değerin tam türünü yazdırır ve bu örnekte aşağıdakini yazdıracaktır;

ModifiedContent<ModifiedContent<Button<Text>, _BackgroundStyleModifier<Color>>, _FrameLayout>

Burada iki şey göreceğiz;

  • Bir view’ı değişitirdiğimizde SwiftUI generics kullanarak bu modifier’ı uygular: ModifiedContent<OurThing, OurModifier>
  • Birden fazla modifier uyguladığımızda, bunlar üst üste yığılır : ModifiedContent<ModifiedContent<…

Tipin ne olduğunu okumak için, en içteki tipten başlayalım ve dışa doğru ilerleyelim;

  • En içteki tür : ModifiedContent<Button<Text>, _BackgroundStyleModifier<Color> butonumuzda arka plan rengi uygulanmış bir metin vardır.
  • Bunun etrafında, ilk view’ı (buton + arka plan rengi) alan ve ona daha büyük bir frame veren ModifiedContent<…, _FrameLayout> var.

Bunun anlamı, modifier sırasının önemli olduğudur. Arka plan rengini frame’den sonra uygulamak için kodumuzu yeniden yazarsak, beklediğimiz sonucu elde edebiliriz.

Button("Hello, world!") {
    print(type(of: self.body))
}
.frame(width: 200, height: 200)
.background(.red)

swiftui modifier order

Şimdilik bunu düşünmenin en iyi yolu, SwiftUI’nin her modifier’dan sonra view’ı oluşturduğunu hayal etmektir. Yani .background(.red) dediğimiz anda, hangi frame’i verdiğimizden bağımsız olarak arka planı kırmızıya boyar. Daha sonra frame’i genişletirsek, arka planı sihirli bir şekilde yeniden çizmez.

Elbette SwiftUI aslında bu şekilde çalışmıyor, çünkü çalışsaydı performans açısından kabus olurdu. Bu sadece öğrenirken kullanmak için zihinsel bir kısayol.

Modifier’ları kullanmanın önemli bir yan etkisi de aynı etkiyi birden fazla kez uygulayabilmemizdir.

Örneğin, SwiftUI bize padding() modifier’ını verir, bu da view’ın etrafına küçük bir boşluk ekler böylece diğer view’lara veya ekranın kenarına doğru itilmez. Önce padding, sonra bir arka plan rengi sonra daha fazla padding ve farklı bir arka plan rengi uygularsak, bir görünüme bunun gibi birden fazla kenarlık verebiliriz.

Text("Hello, world!")
    .padding()
    .background(.red)
    .padding()
    .background(.blue)
    .padding()
    .background(.green)
    .padding()
    .background(.yellow)

swiftui more padding more color

SwiftUI view türü olarak neden “some View” Kullanıyor? #

some View ın opaque return value olduğundan daha önce bahsetmiştik. Bu “View protokolüne uyan bir nesne, ancak ne olduğunu söylemek istemiyoruz” anlamına gelir.

some View döndürmek, hangi görünüm türünün geri döneceğini biz bilmesek de derleyecinin bildiği anlamına gelir. Bu küçük bir şey gibi gözükebilir fakat önemli etkileri vardır.

İlk olarak, some View kullanmak performans açısından önemlidir. SwiftUI’nin gösterdiğimiz view’lara bakabilmesi ve bunların nasıl değiştiğini anlayabilmesi gerekir, böylece kullanıcı arayüzünü doğru bir şekilde güncelleyebilir. SwiftUI bu ekstra bilgiye sahip olmasaydı, SwiftUI’nin tam olarak neyin değiştiğini anlaması çok yavaş olurdu, her küçük değişiklikten sonra her şeyi bırakıp yeniden başlaması gerekirdi.

İkinci fark, SwiftUI’nin ModifiedContent kullanarak verilerini oluşturma şekli nedeniyle önemlidir.

Button("Hello World") {
    print(type(of: self.body))
}
.frame(width: 200, height: 200)
.background(.red)

Bu basit bir buton oluşturur, ardından tam Swift türünü yazdırmasını sağlar ve birkaç ModifiedContent instance’ı ile uzun bir çıktı verir.

View protocol’ün kendisine bağlı bir türü vardır, bu View’ın tek başına bir anlam ifade etmediği anlamına gelir. Tam olarak ne tür bir view olduğunu söylememiz gerekir.

Yani böyle bir view yazmaya izin verilmezken;

struct ContentView: View {
    var body: View {
        Text("Hello, world!")
    }
}

Bu şekilde bir view yazmak ise mümkündür;

struct ContentView: View {
    var body: Text {
        Text("Hello, world!")
    }
}

Sadece View döndürmek mantıklı değildir, çünkü Swift view’ın içinde ne olduğunu bilmek ister. Öte yandan, Text döndürmekte bir problem yok çünkü Swift view’ın ne olduğunu biliyor.

Şimdi daha önce yazdığımız koda geri dönelim;

Button("Hello World") {
    print(type(of: self.body))
}
.frame(width: 200, height: 200)
.background(.red)

Eğer body property’imizde bunlardan birini döndürmek istiyorsak, ne yazmalıyız? Kullanılacak ModifiedContent yapılarının tam kombinasyonunu bulmaya çalışabiliriz, ancak bu gerçekten zordur. Aslında gerçek şu ki, tüm bunlar dahili SwiftUI işleri olduğu için umursamıyoruz.

some View ’ın yapmamıza izin verdiği şey, “bu Button veya Text gibi bir view olacak, ancak ne olduğunu söylemek istemiyorum” demek. Böylece, View ’ın sahip olduğu boşluk gerçek bir view nesnesi tarafından doldurulacaktır, ancak tam uzun türü yazmamız gerekmez.

İşin biraz daha karmaşık hale geldiği iki yer var;

  1. VStack nasıl çalışıyor? View protocol’e uyuyor ve içinde çok sayıda farklı şey içerebiliyorsa ne tür bir içeriğe sahip?
  2. İki view’ı bir stack ile sarmadan doğrudan body property’de göndersek ne olur?

Öncelikle ilk soruyu yanıtlamak gerekirse, içinde iki text view olan bir VStack oluşturursak, SwiftUI bu iki view’ı içeren bir TupleView oluşturur. TupleView içinde tam olarak iki view (bu örnek için) tutan özel bir view türüdür. Böylece, VStack “bu ne tür bir view?” sorusunu “iki text view içeren bir TupleView” olarak cevaplar.

Peki ya VStack içinde üç text view varsa? o zaman üç view içeren bir TupleView olur. Ya da dört view, vaya sekiz view hatta 10 view. TupleView genişlemeye devam eder.

İkinci soruya gelince, Swift sessizce body property’ye @ViewBuilder adında özel bir attribute uygular. Bu, birden fazla view’ı TupleView kapsayıcısı ile sarar. Böylece birden fazla view geri gönderiyor gibi görünsek de bunlar tek bir TupleView’de birleştirilir.

View protocol’e sağ tıklayıp “Jump to Definition” ı seçerseniz, body property’nin gerekliliğini ve @ViewBuilder attribute ile işaretlendiğini görürürüz.

@ViewBuilder @MainActor var body: Self.Body { get }

SwiftUI Koşullu Modifier (Conditional Modifier) #

Modifier’ların yalnızca belirli bir koşul karşılığında uygulanmasını istemek yaygındır ve SwfitUI’de bunu yapmanın en kolay yolu ternary operator kullanmaktır.

Örneğin, true ya da false olabilen bir property’imiz olsaydı, bunu aşağıdaki gibi bir butonun stilini kontrol etmek için kullanabilirdik.

struct ContentView: View {
    @State private var useRedText = false

    var body: some View {
        Button("Hello World") {
            // flip the Boolean between true and false
            useRedText.toggle()            
        }
        .foregroundStyle(useRedText ? .red : .blue)
    }
}

Dolayısıyla, useRedText true olduğunda modifier .foregroundStyle(.red) olur, false olduğunda ise .foregroundStyle(.blue) olur. SwiftUI, @State property’lerdeki değişiklikleri izlediği ve body property’yi yeniden çağırdığı için, bu property her değiştiğinde renk hemen güncellenecektir.

Bazı durumlarda, farklı view’lar döndürmek için genellikle normal if koşullarını kullanabiliriz, ancak bu aslında SwiftUI için daha fazla iş yaratır. Farklı renklerle kullanılan bir Buton görmek yerine şimdi iki farklı Buton view görür. Bu yüzden boolean koşulu değiştirdiğimizde sadece sahip olduğu rengi yeniden renklendirmek yerine, eski butonu yok ederek yeni butonu oluşturacaktır.

Dolayısıyla, bu tür bir kod aynı görünebilir, ancak aslında daha verimsizdir;

var body: some View {
    if useRedText {
        Button("Hello World") {
            useRedText.toggle()
        }
        .foregroundStyle(.red)
    } else {
        Button("Hello World") {
            useRedText.toggle()
        }
        .foregroundStyle(.blue)
    }
}

Bazen if deyimlerinin kullanılması kaçınılmazdır, ancak mümkün olduğunda if yerine ternary operator kullanın.

SwiftUI Environment Modifier #

Kapsayıcılara (container) birçok modifier uygulanabilir, bu da aynı modifier’ı aynı anda bir çok view’a uygulamamıza olanak tanır.

Örneğin, bir VStack ’te dört text view varsa ve hepsine aynı font modifier’ı vermek istiyorsak, modifier’ı doğrudan VStack’e uyguluyabilir ve bu değişikliğin dört text view’a da uygulanmasını sağlayabiliriz;

VStack {
    Text("Gryffindor")
    Text("Hufflepuff")
    Text("Ravenclaw")
    Text("Slytherin")
}
.font(.title)

swiftui environment modifier

Buna environment modifier denir ve bir view’a uygulanan normal bir modifier’dan farklıdır.

Kodlama açısından bakıldığında bu modifier’lar normal modifier’larla tamamen aynı şekilde kullanılır. Ancak, bu child view’lardan herhangi biri aynı modifier’ı geçersiz kılarsa, child sürüm öncelikli olacağından farklı davranırlar.

Örnek olarak, bu dört text view title olarak görüntülenir fakat biri daha büyüktür.

VStack {
    Text("Gryffindor")
        .font(.largeTitle)
    Text("Hufflepuff")
    Text("Ravenclaw")
    Text("Slytherin")
}
.font(.title)

swiftui environment modifier child

Yukarıda en dıştaki font() bir environment modifier’dır, yani “Gryffindor” text view bunu özel bir font ile geçersiz kılabilir.

Aşağıdaki kod VStack ’e bir blur efekt uygular ardından bu efekti bir text view’da devredışı bırakmaya çalışır.

VStack {
    Text("Gryffindor")
        .blur(radius: 0)
    Text("Hufflepuff")
    Text("Ravenclaw")
    Text("Slytherin")
}
.blur(radius: 5)

swiftui environment modifier exception

Yukarıdaki kod aynı şekilde çalışmayacaktır. blur() normal bir modifier’dır. Bu nedenle child view’lara uygulanan blur, VStack blur’unun yerini almak yerine ona eklenir.

Hangi modifier’ın environment modifier, hangilerinin normal modifier olduğunu bilmenin belgeleri okumaktan başka yolu yok.

Property Olarak view #

Karmaşık view hiyerarşilerini oluşturmanın kolay bir yolu, view’ları bir property olarak oluşturup ardından bu property’leri layout içinde kullanmaktır.

Örnek olarak, iki tane text view ‘ı property olarak oluşturup, bunları VStack içinde kullanabiliriz.

struct ContentView: View {
    let motto1 = Text("Draco dormiens")
    let motto2 = Text("nunquam titillandus")

    var body: some View {
        VStack {
            motto1
            motto2
        }
    }
}

Hatta bu property’lere kullanıldıkları yerde doğrudan modifier’da uygulayabiliriz.

VStack {
    motto1
        .foregroundStyle(.red)
    motto2
        .foregroundStyle(.blue)
}

view’ları property olarak oluşturmak, body kodumuzu daha temiz tutmamıza yardımcı olur, aynı zamanda kod tekrarını da azaltır.

Swift, nesne oluşturulduğunda sorunlara neden olacağından, diğer strored property’lere atıfta bulunan stored property oluşturmamıza izin vermez.

Fakat bu şekilde computed property oluşturabiliriz.

var motto1: some View {
    Text("Draco dormiens")
}

Bu genellikle karmaşık view’ları daha küçük parçalara ayırmak için harika bir yoldur. Fakat dikkat etmemiz gerek bir durum var. body property’nin aksine Swift burada @ViewBuilder attribute’ı otomatik olarak uygulayamaz, bu nedenle birden fazla view’ı geri göndermek istiyorsak üç seçeneğimiz vardır.

İlk olarak bunları aşağıdaki gibi bir Stack içine yerleştirebiliriz.

var spells: some View {
    VStack {
        Text("Lumos")
        Text("Obliviate")
    }
}

Bunları özellikle bir stack olarak düzenlemek istemiyorsak, bir Group olarak geri gönderebiliriz. Bu durumda view’ların düzeni, kodumuzun başka bir yerinde nasıl kullandığımıza göre belirlenir.

var spells: some View {
    Group {
        Text("Lumos")
        Text("Obliviate")
    }
}

Üçüncü seçenek olarak @ViewBuilder attribute aşağıdaki gibi kendimiz ekleyebiliriz;

@ViewBuilder var spells: some View {
    Text("Lumos")
    Text("Obliviate")
}

SwiftUI View Composition #

SwiftUI, karmaşık view’ları herhangi bir performans etkisine maruz kalmadan daha küçük view’lara ayırmamızı sağlar. Bu büyük bir view’ı birden fazla küçük view’a ayırabileceğimiz ve SwiftUI’nin bunları bizim için yeniden bir araya getireceği anlamına gelir.

Örneğin bu view’da text view’ı şekillendirmek için özel bir yolumuz var.

struct ContentView: View {
    var body: some View {
        VStack(spacing: 10) {
            Text("First")
                .font(.largeTitle)
                .padding()
                .foregroundStyle(.white)
                .background(.blue)
                .clipShape(.capsule)

            Text("Second")
                .font(.largeTitle)
                .padding()
                .foregroundStyle(.white)
                .background(.blue)
                .clipShape(.capsule)
        }
    }
}

Bu iki text view, metinleri dışında aynı olduğundan bunları aşağıdaki gibi yeni bir custom view’da toplayabiliriz.

struct CapsuleText: View {
    var text: String

    var body: some View {
        Text(text)
            .font(.largeTitle)
            .padding()
            .foregroundStyle(.white)
            .background(.blue)
            .clipShape(.capsule)
    }
}

Daha sonra bu CapsuleText view’ı orijinal view içinde şu şekilde kullanabiliriz.

struct ContentView: View {
    var body: some View {
        VStack(spacing: 10) {
            CapsuleText(text: "First")
            CapsuleText(text: "Second")
        }
    }
}

Elbette, bazı modifier’ları bu custom view’da ekleyebiliriz. CapsuleText’den foregroundStyle() ’ı kaldırıp bunları daha sonra uygulayabiliriz.

VStack(spacing: 10) {
    CapsuleText(text: "First")
        .foregroundStyle(.white)
    CapsuleText(text: "Second")
        .foregroundStyle(.yellow)
}

SwiftUI Custom Modifier #

SwiftUI’de yerleşik modifier’lar olduğu gibi, custom modifier’lar da oluşturabiliriz.

Custom modifier oluşturmak için ViewModifier protocol’e uygun yeni bir struct oluşturun. Bunun tek bir gereksinimi vardır, o da body adında Content kabul eden ve some View döndüren bir methoddur.

Örneğin uygulamamızdaki tüm başlıkların belirli bir stile sahip olması gerektiğini söyleyebiliriz. Bu nedenle önce istediğimizi yapan özel bir ViewModifier struct oluşturmamız gerekir:

struct Title: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.largeTitle)
            .foregroundStyle(.white)
            .padding()
            .background(.blue)
            .clipShape(.rect(cornerRadius: 10))
    }
}

Şimdi bunu modifier() modifier’ı ile kullanabiliriz. Evet “modifier” adında bir modifier 😅

Text("Hello World")
    .modifier(Title())

Custom modifier’lar ile çalışırken View üzerinde bunların kullanımını kolaylaştıran extension’lar oluşturmak genellikle akıllıca bir fikirdir. Örneğin, Title modifier’ı aşağıdaki gibi bir extension ile sarabiliriz,

extension View {
    func titleStyle() -> some View {
        modifier(Title())
    }
}

Artık modifier’ı şu şekilde kullanabiliriz;

Text("Hello World")
    .titleStyle()

Custom modifier’lar, mevcut diğer modifier’ları uygulamaktan çok daha fazlasını yapabilir, gerektiğinde yeni view yapısı da oluşturabilirler. Unutmayın modifier’lar var olanları değiştirmek yerine yeni nesneler döndürür, bu nedenle view’ı bir Stack’e yerleştiren ve başka bir view ekleyen bir tane oluşturabiliriz.

struct Watermark: ViewModifier {
    var text: String

    func body(content: Content) -> some View {
        ZStack(alignment: .bottomTrailing) {
            content
            Text(text)
                .font(.caption)
                .foregroundStyle(.white)
                .padding(5)
                .background(.black)
        }
    }
}

extension View {
    func watermarked(with text: String) -> some View {
        modifier(Watermark(text: text))
    }
}

Yukarıdaki kodu yazdıktan sonra, artık herhangi bir view’a bunun gibi bir filigran ekleyebilriz.

Color.blue
    .frame(width: 300, height: 200)
    .watermarked(with: "Hacking with Swift")

swiftui custom modifier

custom modifier’lar kendi stored property’leri varken, View extension olamaz.


Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 23 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.